利用ICMPv4协议实现一个ping程序

文章目录
  1. 1. 抓包
  2. 2. ICMP协议数据报
  3. 3. ping程序原理
  4. 4. Cpp代码实现
  5. 5. traceroute
  6. 6. 问题记录
  7. 7. 参考

Icmp(Internet Control Message Protocol)协议一般与IP协议结合使用,以便给IP协议提供诊断和控制信息。
Icmp通常被认为是Ip协议的一部分,传输的时候也是被封装在Ip报文内。
我们在判断网络状况时用的ping程序就利用了ICMP协议。接下来先运行系统上的ping程序,用tcpdump抓包查看一下传输的数据。
然后解释一下icmp数据报的各个字段。最后思考一下ping程序的结构,然后用c++实现一个自己的ping程序。

抓包

首先在1号终端运行tcpdump。

1
tcpdump -X > icmp.txt   # -X是以十六进制和ascii显示数据

在2号终端执行ping程序,然后ctrl+C退出ping程序。

1
2
3
4
5
6
7
8
root@yifei:/home/yifei# ping baidu.com
PING baidu.com (220.181.38.148) 56(84) bytes of data.
64 bytes from 220.181.38.148 (220.181.38.148): icmp_seq=1 ttl=44 time=5.36 ms
64 bytes from 220.181.38.148 (220.181.38.148): icmp_seq=2 ttl=44 time=5.30 ms
^C
--- baidu.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 5.300/5.330/5.360/0.030 ms

查看icmp.txt中的icmp数据包,查找到跟220.181.38.148的通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
13:10:07.606759 IP yifei > 220.181.38.148: ICMP echo request, id 7657, seq 1, length 64
0x0000: 4500 0054 e26f 4000 4001 a2ee ac15 05ec E..T.o@.@.......
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
0x0010: dcb5 2694 0800 324a 1de9 0001 af58 195e ..&...2J.....X.^
^^^^^^^^^ *******************
0x0020: 0000 0000 1742 0900 0000 0000 1011 1213 .....B..........
0x0030: 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223 .............!"#
0x0040: 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233 $%&'()*+,-./0123
0x0050: 3435 3637 4567
13:10:07.612103 IP 220.181.38.148 > yifei: ICMP echo reply, id 7657, seq 1, length 64
0x0000: 4500 0054 e26f 4000 2c01 b6ee dcb5 2694 E..T.o@.,.....&.
0x0010: ac15 05ec 0000 3a4a 1de9 0001 af58 195e ......:J.....X.^
0x0020: 0000 0000 1742 0900 0000 0000 1011 1213 .....B..........
0x0030: 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223 .............!"#
0x0040: 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233 $%&'()*+,-./0123
0x0050: 3435 3637 4567

可以看到本机先给baidu发送一个ICMP请求,然后baidu给回复了一条消息。
在第一个数据包中,用^标示的是ip首部,用*标示的是icmp首部,剩下的是icmp数据,具体每个字段的意义,在本文下一节中讲解。

ICMP协议数据报

下图是icmp报文被ip协议封装后的结构:

1
2
3
----------------------------------------
| Ipv4头部 | Icmp头部 | Icmp数据 |
----------------------------------------

下图为icmp协议头部的结构:

1
2
3
4
5
6
7
|------------------------------------------------|
| 类型(8位) | 代码(8位) | 校验和(16位) |
|------------------------------------------------|
| 依赖类型和代码的内容 |
|------------------------------------------------|
| 数据(可选) |
|------------------------------------------------|

ping程序的回显请求中,类型为8,代码为0时,结构是下图这样,多了标识符和序列号两个字段:

1
2
3
4
5
6
7
|------------------------------------------------|
| 类型(8位) | 代码(8位) | 校验和(16位) |
|------------------------------------------------|
| 标识符(16位) | 序列号(16位) |
|------------------------------------------------|
| 数据(可选) |
|------------------------------------------------|

  • 其中的类型和代码字段都是8位,这两个字段决定了Icmp数据报的用途,两个字段表示的组合如下:
    20200106 icmp.jpeg
  • 校验和16位,涵盖了icmp的报头和数据两部分。
  • 标识符可以设置为进程pid号,因为没有端口号来区分数据报了。
  • 序列号可以是从0开始的序号,用来标识icmp数据报。

ping程序原理

了解了icmp协议之后,ping程序的原理就很好理解了,可以分为以下几步。
1.将输入的域名转为ip地址。
2.填充icmp数据报。
3.创建原始套接字。
4.使用sendoto发送icmp报文。
5.使用recvfrom接受数据报。

Cpp代码实现

  • code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    #include <iostream>
    #include <cstring>
    #include <unistd.h>
    #include <netdb.h>
    #include <errno.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <ctime>
    #include <signal.h>
    using namespace std;

    enum{ICMP_DATA_LEN=1024};
    enum{IP_DATA_LEN=1024};
    enum{RECV_MAX_LEN=1024};

    /* 暂时用不着ip头部的结构体
    //IP报头
    typedef struct{
    unsigned char hdr_len; //4位头部长度 & 4位版本号
    unsigned char tos; //8位服务类型
    unsigned short total_len; //16位总长度
    unsigned short identifier; //16位标识符
    unsigned short frag_and_flags; //3位标志加13位片偏移
    unsigned char ttl; //8位生存时间
    unsigned char protocol; //8位上层协议号
    unsigned short checksum; //16位效验和
    unsigned int sourceIP; //32位源IP地址
    unsigned int destIP; //32位目的IP地址
    }IpHeader;

    //整个ip数据报
    typedef struct{
    IpHeader ih;
    char data[IP_DATA_LEN];
    }IpComplete;
    */

    //ICMP报头
    typedef struct{
    unsigned char type; //8位类型字段
    unsigned char code; //8位代码字段
    unsigned short cksum=0; //16位效验和
    unsigned short id; //16位标识符
    unsigned short seq; //16位序列号
    }IcmpHeader;

    //整个icmp数据报
    typedef struct{
    IcmpHeader icmph;
    char data[ICMP_DATA_LEN];
    }IcmpComplete;

    pid_t pid; //当前进程pid
    int seqnum=0; //icmp头部中的序列号
    struct sockaddr_in sai; //目的ip地址
    char ipaddr[20]; //char类型ip地址
    clock_t second,first; //记录每个数据报发出~接受到回复的时间
    int lostpacks=0,sumpacks=0;//记录数据报总数和丢包数

    //计算校验和
    unsigned short CheckSum(unsigned short *addr,int len){
    int nleft=len;
    unsigned int sum=0;
    unsigned short *w=addr;
    unsigned short answer=0;
    while(nleft>1){
    sum+=*w++;
    nleft-=2;
    }
    if(nleft==1){
    *(unsigned short *)(&answer)=*(unsigned char *)w;
    sum+=answer;
    }
    sum=(sum>>16)+(sum&0xffff);
    sum+=(sum>>16);
    answer=~sum;
    return answer;
    }

    //填充整个icmp数据报
    void FillIcmpCom(IcmpComplete *icmpcom){
    memset(icmpcom,0,8+sizeof(icmpcom->data));
    icmpcom->icmph.type=(unsigned char)8;
    icmpcom->icmph.code=0;
    icmpcom->icmph.cksum=0;
    pid=getpid();
    icmpcom->icmph.id=pid;
    icmpcom->icmph.seq=seqnum;
    icmpcom->icmph.cksum=CheckSum((unsigned short*)icmpcom,sizeof(icmpcom));
    }

    //根据域名获取ip地址
    void GetIpByName(char *argv){
    struct addrinfo*res;
    if(getaddrinfo(argv,nullptr,nullptr,&res)!=0){
    cout<<"地址解析出错!"<<endl;
    return ;
    }else{
    //网络字节序的ip地址转为ascii字符数组中点分十进制的ip地址
    inet_ntop(AF_INET,(void*)&((sockaddr_in*)(res->ai_addr))->sin_addr,ipaddr,16);
    sai.sin_family=AF_INET;
    sai.sin_addr.s_addr=inet_addr(ipaddr);
    cout<<"PING "<<argv<<"("<<ipaddr<<")"<<endl;
    }
    freeaddrinfo(res);
    }

    //处理SIGINT信号
    void handler(int data){
    cout<<endl<<"------------statistics-------------"<<endl;
    cout<<sumpacks<<" packets trasimitted,"<<lostpacks<<" packets loss"<<endl;
    exit(0);
    }

    int main(int argc,char **argv){
    GetIpByName(argv[1]);

    int sockfd;
    //创建icmp协议的原生套接字,需要自己构造icmp头部(ip头部系统自动填充,可以通过设置IP_HDRINCL套接字选项自己构造ip头部)
    sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
    if(sockfd<0){
    cout<<"创建socket出错。"<<endl;
    return 0;
    }

    struct timeval time_out;
    time_out.tv_sec=1;
    time_out.tv_usec=0;
    //设置recvfrom函数的接收超时为1秒。
    if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,(void*)&time_out,sizeof(time_out))<0){
    cout<<"设置接收超时出错!"<<endl;
    }
    int errnum=0;
    char recbuf[RECV_MAX_LEN];
    IcmpComplete icmpcomplete;
    signal(SIGINT,handler); //设置ctrl+c中断的处理函数
    for(;;){
    seqnum++;
    FillIcmpCom(&icmpcomplete);

    //发送整个icmp数据报
    if((errnum=sendto(sockfd,(void*)&icmpcomplete,sizeof(icmpcomplete),0,(struct sockaddr *)&sai,sizeof(sai)))<0){
    cout<<"ICMP数据报发送失败! "<<errno<<" "<<strerror(errno)<<endl;;
    return 0;
    }else{
    first=clock();
    }

    //收到的recbuf数据包括ip头部,icmp头部,icmp数据
    if(recvfrom(sockfd,recbuf,sizeof(recbuf),0,nullptr,nullptr)<0){
    cout<<"*"<<endl;
    lostpacks++;
    }else{
    second=clock();
    cout<<sizeof(recbuf)<<" bytes from("<<ipaddr<<"): "
    <<"icmp_seq="<<seqnum<<" "
    <<"ttl="<<(int)recbuf[8]<<" "
    <<"time="<<(second-first)<<" ms"<<endl;
    }
    sleep(1);
    sumpacks++;
    }
    return 0;
    }
  • 编译运行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    \> gcc myping.cpp -o myping
    \> ./myping www.baidu.com
    PING baidu.com(39.156.69.79)
    1024 bytes from(39.156.69.79): icmp_seq=1 ttl=40 time=73 ms
    1024 bytes from(39.156.69.79): icmp_seq=2 ttl=40 time=47 ms
    1024 bytes from(39.156.69.79): icmp_seq=3 ttl=40 time=40 ms
    1024 bytes from(39.156.69.79): icmp_seq=4 ttl=40 time=39 ms
    1024 bytes from(39.156.69.79): icmp_seq=5 ttl=40 time=40 ms
    ^C
    ------------statistics-------------
    4 packets trasimitted,0 packets loss

traceroute

traceroute工具可以查看数据报在传输过程中都是经过了哪些节点。这利用了ip协议中的ttl字段。
traceroute首先发送ttl字段为1的ip数据报,然后分别发送ttl字段为2,3,…的数据报。
当数据报经过一个节点,ttl变为0之后,就会给发送方回复一个icmp数据报。这样就能获得每个节点的ip地址了。
将之前的程序进行简单改动,就能实现traceroute的效果。

1
2
3
1.在每次sendto发送icmp数据报之前,使用该函数可以直接设置IP数据报中的ttl字段,不用自己填充ip头部了。
setsockopt(sockfd,IPPROTO_IP,IP_TTL,(void*)&ittl,sizeof(ittl))
2.接受到返回的消息后,输出ip头部中的源ip地址。

问题记录

  • sendto函数的地址参数,直接传入getaddrinfo()得到的res->ai_addr,总是返回invalid argument错误,自己填充一个sockaddr_in传入就好了。
  • 自己的ping程序测试的time要比Linux自带的高一些。

参考

  • 《TCP/IP详解(卷1:协议)》第二版

欢迎与我分享你的看法。
转载请注明出处:http://taowusheng.cn/